package com.googlecode.dgwt.client.dojo.impl;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.googlecode.dgwt.client.dojo.Dojo;
import com.googlecode.dgwt.client.dojo.NodeList;
import com.googlecode.dgwt.client.dojo.base.BoxModel;
import com.googlecode.dgwt.client.dojo.base.BrowserSensitive;
import com.googlecode.dgwt.client.dojo.base.BrowserSensitive.ReplaceWith;
import com.googlecode.dgwt.client.dojo.base.BrowserSensitive.WhenUserAgent;
import com.googlecode.dgwt.client.dojo.core.JsArray;
import com.googlecode.dgwt.client.dojo.core.JsMap;
import com.googlecode.dgwt.client.dojo.core.JsObject;
import com.googlecode.dgwt.client.dojo.string.RegExp;
import com.googlecode.dgwt.client.dojo.string.RegExpResult;
import com.googlecode.dgwt.client.dojo.util.JsUtil;

/**
 * Provides basic functionalities adapted from native Dojo framework.
 * 
 * @author wellsantos@wellsantos.com
 * @since 0.01
 */
@ReplaceWith({
    @WhenUserAgent(match="ie\\v", type=DojoImpl_IE.class),
    @WhenUserAgent(match="safari\\v", type=DojoImpl_WebKit.class)
})
public class DojoImpl implements BrowserSensitive {
    
    
    protected static String TEMP_CONTAINER_PROPERTY_NAME = "$$_Dojo_TEMP_CONTAINER";
    
    
    public final HtmlImpl html = GWT.create(HtmlImpl.class);
    public final StringImpl string = GWT.create(StringImpl.class);
    public final ReflectionImpl reflect = GWT.create(ReflectionImpl.class);
    public final NavigatorImpl navigator = GWT.create(NavigatorImpl.class);

    // support stuff for toDom()
    protected JsMap<JsArray<String>> tagWrap;
    protected RegExp reTag = RegExp.create("<\\s*([\\w\\:]+)");

    // support stuff for getProp()
    protected RegExp objectPropSep = RegExp.create("\\.");
    
    
    /**
     * Box functions will assume this model.
     * On IE/Opera, BORDER_BOX will be set if the primary document is in quirks mode.
     * Can be set to change behavior of box setters.
     */
    public BoxModel boxModel = BoxModel.CONTENT_BOX;
    
    
    /**
     * Dojo implementation
     */
    public DojoImpl(){
        setupBoxModel();
        initDOMEngine();
    }
    

    /**
     * Initialize support stuff for dojo._toDom
     */
    protected native void initDOMEngine() /*-{
        
        var tagWrap = this.@com.googlecode.dgwt.client.dojo.impl.DojoImpl::tagWrap = {
            option: ["select"],
            tbody: ["table"],
            thead: ["table"],
            tfoot: ["table"],
            tr: ["table", "tbody"],
            td: ["table", "tbody", "tr"],
            th: ["table", "thead", "tr"],
            legend: ["fieldset"],
            caption: ["table"],
            colgroup: ["table"],
            col: ["table", "colgroup"],
            li: ["ul"]
        };
        

        // generate start/end tag strings to use
        // for the injection for each special tag wrap case.
        for(var param in tagWrap){
            var tw = tagWrap[param];
            tw.pre  = param == "option" ? '<select multiple="multiple">' : "<" + tw.join("><") + ">";
            tw.post = "</" + tw.reverse().join("></") + ">";
            // the last line is destructive: it reverses the array,
            // but we don't care at this point
        }
    }-*/;
    
    
    /**
     * Gets a temporary element to perform DOM operations
     * @return the element
     */
    protected Element getTempContainer(Document document){
        
        if (document == null){
            document = Document.get();
        }
        
        JsMap<Element> docMap = document.cast();
        Element domContainer = docMap.get(TEMP_CONTAINER_PROPERTY_NAME);
        
        if (domContainer == null){
            domContainer = document.createDivElement();
            docMap.set(TEMP_CONTAINER_PROPERTY_NAME, domContainer);
        }
        
        return domContainer;
    }

    
    /**
     * (non-Javadoc)
     * @see Dojo#getComputedStyle(Node)
     */
    public native <V extends Object> JsMap<V> getComputedStyle(Node node) /*-{
        return (
            node.nodeType == @com.google.gwt.dom.client.Node::ELEMENT_NODE ? 
            node.ownerDocument.defaultView.getComputedStyle(node, null) :
            @com.googlecode.dgwt.client.dojo.core.JsObject::EMPTY_OBJECT
        ); 
    }-*/;
    
    
    /**
     * (non-Javadoc)
     * @see #setObject(String, Object, Object)
     * @see #getObject(String, boolean, Object)
     */
    protected native JsObject getProp(JsArray<String> parts, boolean create, Object context) /*-{
        try {
            var obj=context || $wnd;
            for (var i=0, p; obj && (p=parts[i]); i++){
                obj = (p in obj ? obj[p] : (create ? obj[p]={} : null));
            }
            return obj; // mixed
        } catch (ex) {
            return null;
        }
    }-*/;


    /**
     * (non-Javadoc)
     * @see Dojo#setBoxModel(BoxModel)
     */
    protected void setupBoxModel(){
        
        // Opera documentation claims that Opera 9 uses border-box in BackCompat mode.
        // but experiments (Opera 9.10.8679 on Windows Vista) indicate that it actually continues to use content-box.
        // IIRC, earlier versions of Opera did in fact use border-box.
        // Opera guys, this is really confusing. Opera being broken in quirks mode is not our fault.

        if(navigator.isIEQuirks() /*|| navigator.isOpera() && navigator.isQuirks()*/){
            
            // client code may have to adjust if compatMode varies across iframes
            boxModel = BoxModel.BORDER_BOX;
        }
    }
    

    /**
     * (non-Javadoc)
     * @see Dojo#addClass(Element, String)
     */
    public void addClass(Element node, String className) {
        String cls = JsUtil.or(node.getClassName(),"");
        className = className == null ? "" : className.trim();
        
        if(!className.isEmpty() 
                && (" "+ cls +" ").indexOf(" " + className + " ") < 0){
            node.setClassName((!cls.isEmpty() ? cls+" " : "") + className);
        }
    }

    
    /**
     * (non-Javadoc)
     * @see Dojo#destroy(Node)
     */
    public void destroy(Node node) {
        /*
        if (JsUtil.isNotEmpty(node)) {
            try{
                Element domContainer = getTempContainer(node.getOwnerDocument());
                domContainer.appendChild(node.getParentNode() != null ? node.getParentNode().removeChild(node) : node);
                
                // FIXME: NOTE: see http://trac.dojotoolkit.org/ticket/2931. This may be a bug and not a feature
                domContainer.setInnerHTML("");
                
            }catch(Throwable e){
                // squelch
            }
        }
        */
    }

    
    /**
     * (non-Javadoc)
     * @see Dojo#destroy(Object)
     */
    public void destroy(Object obj) {
        /*
        if (JsUtil.isNotEmpty(obj)) {
            if (obj instanceof Releasable){
                if (GWT.isScript()){
                    ((Releasable) obj).release();
                    JsUtil.destroy(obj);
                }
            }
            else
            // fIXME(wellsantos) instanceof funciona com JsObject ?
            if (obj instanceof JavaScriptObject){
                if (Node.is((JavaScriptObject)obj)){
                    destroy((Node) obj);
                } else {
                    JsUtil.destroy(obj);
                }
            }
        }
        */
    }

    
    /**
     * (non-Javadoc)
     * @see Dojo#exists(String, Object)
     */
    public boolean exists(String name, Object scope) {
        return JsUtil.isNotEmpty(getObject(name, false, scope));
    }
    

    /**
     * (non-Javadoc)
     * @see Dojo#getObject(String, boolean, Object))
     */
    @SuppressWarnings("unchecked")
    public <T extends Object> T getObject(String name, boolean create, Object scope) {
        return (T) getProp(objectPropSep.split(name), create, scope);
    }

    
    /**
     * (non-Javadoc)
     * @see Dojo#isBodyLTR()
     */
    public boolean isBodyLTR() {
        JsMap<String> cs = getComputedStyle(Document.get().getBody());
        return "ltr".equals(cs.get("direction"));
    }


    /**
     * (non-Javadoc)
     * @see Dojo#setObject(String, Object, Object)
     */
    public <T extends Object> T setObject(String name, T value, Object scope) {
        
        JsArray<String> parts = objectPropSep.split(name);
        String p=parts.pop();
        
        JsMap<T> obj = getProp(parts, true, scope).cast();
        return JsUtil.and(obj, p) ? (obj.put(p, value)) : null; // Object
    }


    /**
     * (non-Javadoc)
     * @see Dojo#query(String, Node)
     */
    public native <T extends Node> NodeList<T> query(String query, Object parent, NodeList<? extends Node> nodeList) /*-{
        var q = dojo.query(query, parent), l = q.length;
        var r = nodeList;
        for (var i=0; i<l; i++){
            r.@com.googlecode.dgwt.client.dojo.NodeList::add(Ljava/lang/Object;)(q[i]);
        }
        return r;
    }-*/;

    
    public <T extends Node> NodeList<T> query(String query, Object parent) {
        return query(query, parent, new NodeList<Node>());
    }
    
    
    /**
     * (non-Javadoc)
     * @see Dojo#removeClass(Element, String)
     */
    public boolean hasClass(Element node, String className) {
        return JsUtil.and(className, ((" "+ JsUtil.or(node.getClassName(),"") +" ").indexOf(" "+ className +" ") >= 0));
    }

    
    /**
     * (non-Javadoc)
     * @see Dojo#removeClass(Element, String)
     */
    public void removeClass(Element node, String className) {
        String cls = JsUtil.or(node.getClassName(),"");
        String t = ((" " + cls + " ").replace(" " + className + " ", " ")).trim();
        
        if (!cls.equals(t)){
            node.setClassName(t); 
        }
    }

    
    /**
     * (non-Javadoc)
     * @see Dojo#getStyle(Node, String)
     */
    public native String getStyle(Node node, String name) /*-{
        var value = dojo.style(node, name);
        return (undefined == value || null == value) ? null : ("" + value);
    }-*/;
    
    
    /**
     * (non-Javadoc)
     * @see Dojo#setStyle(Node, String, String)
     */
    public native void setStyle(Node node, String name, String value) /*-{
        if (name){
            if ("width" == name || "height" == name){
                if (value){
                    // filtrar Width e Height negativos para não dar erro no IE
                    // TODO : por isso em DojoImpl_IE
                    value = value.replace(/^\s*-/, "");
                }
            }
            dojo.style(node, name, value);
        }
    }-*/;

    
    /**
     * Converts HTML string into DOM nodes.
     * @see Dojo#toDom(String)
     */
    public Node toDom(String html, Document doc, String multipleNodesContainerTagName) {

        if (doc == null){
            doc = Document.get();
        }

        if (html == null){
            html = "";
        }

        // find the starting tag, and get node wrapper
        RegExpResult match = reTag.exec(html);
        String tag = match != null ? match.get(1).toLowerCase() : "";
        Element master = getTempContainer(doc);
        JsArray<String> wrap; 
        int i;
        Element fc, df;
        
        if(match != null && tagWrap.containsKey(tag)){
            wrap = tagWrap.get(tag);
            master.setInnerHTML(wrap.get("pre") + html + wrap.get("post"));
            for(i = wrap.length(); i != 0; --i){
                master = master.getFirstChild().cast();
            }
        }else{
            master.setInnerHTML(html);
        }

        // one node shortcut => return the node itself
        if(master.getChildNodes().getLength() == 1){
            return master.removeChild(master.getFirstChild()); // DOMNode
        }
        
        // return multiple nodes as a document fragment ?
        if (multipleNodesContainerTagName == null){
            df = JsUtil.createDocumentFragment(doc);
        } else {
            df = doc.createElement(multipleNodesContainerTagName);
        }
        
        while((fc = master.getFirstChild().cast()) != null){ // intentional assignment
            df.appendChild(fc);
        }
        return df; // DOMNode
    }

    
    /**
     * (non-Javadoc)
     * @see Dojo#onModuleLoad()
     */
    public void onModuleLoad(){
        loadFromTopWindow();		
    }

    
    /**
     * Loads native dojo from top window
     */
    private native void loadFromTopWindow() /*-{
        window.dojo = $wnd.dojo;
        window.dijit = $wnd.dijit;
        window.dojox = $wnd.dojox;
        
        try {
            window.console = $wnd.console;
        } catch (ex) {
            // firebug 1.4 não permite 
            // alterar a variável console
        }
    }-*/;


    public <T extends Cloneable> T clone(T obj) {
        if (GWT.isScript()) {
            return nativeClone(obj);
        }
        return reflect.clone(obj); 
    }
    
    
    protected native <T extends Cloneable> T nativeClone(T obj) /*-{
        return dojo.mixin({}, obj);
    }-*/;

}
